import {
    _decorator,
    Component,
    EventMouse,
    geometry,
    input,
    Input,
    math,
    Node,
    PhysicsRayResult,
    PhysicsSystem,
    Vec3,
} from 'cc';
const { ccclass, property } = _decorator;

@ccclass('ThirdPersonCamera')
export class ThirdPersonCamera extends Component {
    @property(Node)
    target: Node | null = null;

    @property
    distance: number = 5;

    @property
    theta: number = 45;

    @property
    phi: number = 30;

    private tempTheta = this.theta;
    private tempPhi = this.phi;

    private smoothRotationSpeed: number = 0.5; // 旋转平滑速度

    private isMouseDown: boolean = false;
    private lastMouseX: number = 0;
    private lastMouseY: number = 0;

    private currentPosition: Vec3 = new Vec3();
    private smoothSpeed: number = 0.1;

    onLoad() {
        PhysicsSystem.instance.enable = true;
        input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
        input.on(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
        input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this);
        input.on(Input.EventType.MOUSE_WHEEL, this.mouseWheel, this);
    }

    onDestroy() {
        input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
        input.off(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
        input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this);
    }

    protected lateUpdate(dt: number): void {
        this.updateCamera();
    }

    private onMouseDown(event: EventMouse) {
        this.isMouseDown = true;
        this.lastMouseX = event.getLocationX();
        this.lastMouseY = event.getLocationY();
    }

    private onMouseMove(event: EventMouse) {
        if (!this.isMouseDown || !this.target) return;
        const deltaX = event.getLocationX() - this.lastMouseX;
        const deltaY = event.getLocationY() - this.lastMouseY;
        this.tempTheta += deltaX * 0.1;
        this.tempPhi -= deltaY * 0.1;
        this.tempPhi = math.clamp(this.tempPhi, 0 + 10, 170 - 10);
        this.lastMouseX = event.getLocationX();
        this.lastMouseY = event.getLocationY();
    }

    private onMouseUp() {
        this.isMouseDown = false;
    }

    private mouseWheel(event: EventMouse) {
        let sy = event.getScrollY();
        this.distance -= Math.sign(sy) * 0.2;
        this.distance = math.clamp(this.distance, 3, 15);
    }

    private updateCamera() {
        if (!this.target) return;

        const idealPosition = this.calculateOrbitPosition(
            this.target,
            this.distance,
            this.theta,
            this.phi,
        );

        const hitDistance = checkOcclusion(
            this.node,
            this.target,
            0xffffffff,
        );
        let targetPosition = idealPosition;

        if (hitDistance !== -1) {
            const targetDistance = Vec3.distance(
                idealPosition,
                this.target.worldPosition,
            );
            const newDistance = hitDistance * 0.9;
            targetPosition = Vec3.lerp(
                new Vec3(),
                idealPosition,
                this.target.worldPosition,
                newDistance / targetDistance,
            );
        }

        Vec3.lerp(
            this.currentPosition,
            this.currentPosition,
            targetPosition,
            this.smoothSpeed,
        );

        this.node.setWorldPosition(this.currentPosition);
        this.lookAtTarget(this.node, this.target);
    }

    private calculateOrbitPosition(
        target: Node,
        distance: number,
        theta: number,
        phi: number,
    ): Vec3 {
        this.theta = math.lerp(
            this.theta,
            this.tempTheta,
            this.smoothRotationSpeed,
        );
        this.phi = math.lerp(
            this.phi,
            this.tempPhi,
            this.smoothRotationSpeed,
        );
        const radPhi = math.toRadian(phi);
        const radTheta = math.toRadian(theta);
        const offsetX = distance * Math.sin(radPhi) * Math.sin(radTheta);
        const offsetY = distance * Math.cos(radPhi);
        const offsetZ = distance * Math.sin(radPhi) * Math.cos(radTheta);
        return new Vec3(
            target.worldPosition.x + offsetX,
            target.worldPosition.y + offsetY,
            target.worldPosition.z + offsetZ,
        );
    }

    private lookAtTarget(node: Node, target: Node) {
        node.lookAt(target.worldPosition);
    }
}

/**
 * 检测摄像机和目标之间是否有遮挡
 * @param camera 摄像机节点
 * @param target 目标节点
 * @returns 如果被遮挡，返回命中点距离；否则返回 -1
 */
function checkOcclusion(camera: Node, target: Node, mask: number): number {
    PhysicsRayResult;
    const ray = new geometry.Ray();
    ray.o = camera.worldPosition;
    ray.d = Vec3.subtract(
        new Vec3(),
        target.worldPosition,
        camera.worldPosition,
    ).normalize();

    const distance = Vec3.distance(
        target.worldPosition,
        camera.worldPosition,
    );

    let b = PhysicsSystem.instance.raycastClosest(
        ray,
        mask,
        distance,
        true,
    );
    if (b) {
        const hitResult = PhysicsSystem.instance.raycastClosestResult;
        const hitDistance = Vec3.distance(ray.o, hitResult.hitPoint);
        const targetDistance = Vec3.distance(
            camera.worldPosition,
            target.worldPosition,
        );
        if (hitDistance < targetDistance) {
            return hitDistance;
        }
    }

    return -1;
}
